﻿// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 
// See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text;
using System.Threading.Actors.Languages;
using System.Threading.Tasks;

namespace System.Threading.Actors.Utilities
{
    // Common logic for handling method calls within an IActorState implementation.
    // Includes common support for dynamic C# methods, adding language packs, and
    // adding/calling methods from other languages.
    public class MethodCallHelper
    {
        private readonly IActorState m_state;

        // Support for methods in arbitrary languages.  However, most of the time we won't need to 
        // spin up a new interpreter or VM and its libraries.  So, let's keep a set of uninitialized langauge packs and promote
        // them to "active" only when someone adds a method in that particular language pack.
        private readonly Dictionary<SupportedLanguages, ILanguagePackage> m_languagePacks = new Dictionary<SupportedLanguages, ILanguagePackage>();
        private readonly Dictionary<SupportedLanguages, Func<ILanguagePackage>> m_uninitializedLanguagePacks = new Dictionary<SupportedLanguages, Func<ILanguagePackage>>();

        private readonly DynamicActorStateWrapper m_dynamicStateProjection;

        public MethodCallHelper(IActorState state)
        {
            Contract.Requires(state != null);

            m_state = state;
            m_dynamicStateProjection = new DynamicActorStateWrapper(m_state);
        }

        // Convenience method on the interface
        public object CallMethod(string name, object[] parameters)
        {
            var tempObject = default(object);
            if (m_state.TryGet(name, out tempObject))
            {
                var dynamicMethod = tempObject as Func<dynamic, dynamic[], object>;
                if (dynamicMethod != null) return dynamicMethod(m_dynamicStateProjection, parameters);

                var method = tempObject as Func<IActorState, object[], object>;
                if (method != null) return method(m_state, parameters);

                throw new InvalidOperationException(String.Format("Key \"{0}\" exists in the actor, but is not a method.", name));
            }
            else
            {
                // Try to find the method name in available language packs
                // Assumption is that we only need to search through active language packs - no uninitialized language pack needs to be 
                // searched because smoeone would have needed to call AddMethod first on that language pack, making it active.
                lock (m_languagePacks)
                {
                    foreach (var kvp in m_languagePacks)
                    {
                        var pack = kvp.Value;
                        if (pack.OwnsMethod(name)) return pack.CallMethod(name, parameters);
                    }
                }
            }

            // The requested method does not exist
            var assemblyKeyNames = m_state.GetAllKeysWithPrefix("$ASSEMBLY$");
            StringBuilder assemblies = new StringBuilder("Assemblies: { ");
            foreach (String s in assemblyKeyNames)
            {
                assemblies.Append(s);
                assemblies.Append(", ");
            }
            assemblies.Append("}");

            StringBuilder methods = new StringBuilder("Methods: { ");
            foreach (String m in m_state.GetAllKeysWithPrefix("$METHOD$"))
            {
                methods.Append(m);
                methods.Append(", ");
            }
            methods.Append("}");

            if (name != "SomeBogusMethod")
            {
                Debugger.Break();
                bool secondTry = m_state.TryGet(name, out tempObject);
                Contract.Assume(false, "About to throw because we called a method that doesn't exist!  " + assemblies);
                // Attach here and see what's going on?  Is this method really not in our state?
                // If so, are we currently populating our state with data from another replica, and we missed a wait somewhere?
                // This code was added when we were swamping the threadpool with reconnect requests, and possibly starved
                // normal replication.
            }
            throw new InvalidOperationException(String.Format("No method named \"{0}\" exists in this actor.\r\n{1}\r\n{2}", name, assemblies, methods));
        }

        public void AddLanguagePack(SupportedLanguages language, Func<ILanguagePackage> pack)
        {
            m_uninitializedLanguagePacks[language] = pack;
        }

        public void AddMethod(SupportedLanguages language, string methodName, string methodImpl)
        {
            ILanguagePackage pack;
            if (!m_languagePacks.TryGetValue(language, out pack))
            {
                lock (m_languagePacks)
                {
                    if (!m_languagePacks.TryGetValue(language, out pack))
                    {
                        Func<ILanguagePackage> initializationFunc = null;
                        if (m_uninitializedLanguagePacks.TryGetValue(language, out initializationFunc))
                        {
                            pack = initializationFunc();
                            Thread.MemoryBarrier();
                            m_languagePacks.Add(language, pack);
                            m_uninitializedLanguagePacks.Remove(language);
                        }
                    }
                }
            }

            if (pack != null)
            {
                pack.AddMethod(methodName, methodImpl);
            }
            else throw new InvalidOperationException("No support for language " + language);
        }

        public Dictionary<SupportedLanguages, ILanguagePackage> LanguagePacks { get { return m_languagePacks; } }
    }

}
